package ch.ms.persistenttimers.persistence;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xmlpull.v1.XmlSerializer;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.util.Xml;
import ch.ms.persistenttimers.PersistentTimer.PersistentTimerData;
import ch.ms.persistenttimers.actions.AbstractTimerAction;

/*
 * Copyright 2011 dunhill

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
 */

public class PersistentTimerPersistenceManager implements IPersistentTimerPersistenceManager {
	
	private final String PREFERENCES_TIMERKEY_PERFIX = "timer:";
	private final String PREFERENCES_TIMER_LAST_ID = "lastid";
	private final String PREFERENCES_TIMER_UNIQUE_NAMES = "uniqueNames";
	
	private final String XML_ROOT = "timer";
	private final String XML_TIMER_DATA = "data";
	private final String XML_TIMER_DATA_ATTRIBUTE = "attribute";
	
	private final String XML_TIMER_ACTIONS = "actions";
	
	private final String XML_TIMER_ACTIONS_ACTION = "action";
	
	private final String XML_TIMER_ACTIONS_ACTION_TYPE = "type";
	private final String XML_TIMER_ACTIONS_ACTION_TYPE_ONTICK = "ontick";
	private final String XML_TIMER_ACTIONS_ACTION_TYPE_ONFINISH = "onfinish";
	
	private final String XML_TIMER_ACTIONS_ACTION_CLASS = "clazz";

	public PersistentTimerPersistenceManager(Context context) {
		this.context = context;
	}
	
	private Context context = null;

	/**
	 * Pretty awfull implementation of a xml serialization.. imo
	 */
	@Override
	public void persistTimerData(PersistentTimerData data) {
		
		try {
			StringWriter writer = new StringWriter();
			
			XmlSerializer serializer = Xml.newSerializer();
			serializer.setOutput(writer);
			serializer.startDocument("UTF-8", true);
			serializer.startTag(null, XML_ROOT);
				serializer.startTag(null, XML_TIMER_DATA);
					serializeAttribute(serializer,"endless",""+data.isEndless());
					serializeAttribute(serializer,"id",""+data.getId());
					serializeAttribute(serializer,"ticks",""+data.getTicks());
					serializeAttribute(serializer,"currentTicks",""+data.getCurrentTicks());
					serializeAttribute(serializer,"timerStartedDate",""+data.getTimerStartedDate().getTime());
					serializeAttribute(serializer,"delay",""+data.getDelay());
					serializeAttribute(serializer,"forceTicks",""+data.isForceTicks());
					serializeAttribute(serializer,"uniqueName",""+data.getUniqueName());
				serializer.endTag(null, XML_TIMER_DATA);
				
				serializer.startTag(null, XML_TIMER_ACTIONS);
					for(AbstractTimerAction action : data.getOnTickActions()) {
						if(action instanceof ISerializableTimerAction) {
							serializer.startTag(null, XML_TIMER_ACTIONS_ACTION);
								serializer.attribute(null, XML_TIMER_ACTIONS_ACTION_TYPE, XML_TIMER_ACTIONS_ACTION_TYPE_ONTICK);
								serializer.attribute(null, XML_TIMER_ACTIONS_ACTION_CLASS, action.getClass().getName());
								ISerializableTimerAction ser = (ISerializableTimerAction)action;
								ser.serialize(serializer);
							serializer.endTag(null, XML_TIMER_ACTIONS_ACTION);
						}
					}
					
					for(AbstractTimerAction action : data.getOnFinishActions()) {
						if(action instanceof ISerializableTimerAction) {
							serializer.startTag(null, XML_TIMER_ACTIONS_ACTION);
								serializer.attribute(null, XML_TIMER_ACTIONS_ACTION_TYPE, XML_TIMER_ACTIONS_ACTION_TYPE_ONFINISH);
								serializer.attribute(null, XML_TIMER_ACTIONS_ACTION_CLASS, action.getClass().getName());
								ISerializableTimerAction ser = (ISerializableTimerAction)action;
								ser.serialize(serializer);
							serializer.endTag(null, XML_TIMER_ACTIONS_ACTION);
							
						}
						
						
						
					}
					
				serializer.endTag(null, XML_TIMER_ACTIONS);
			serializer.endTag(null, XML_ROOT);
			serializer.endDocument();
			
			serializer.flush();
			
			String xmlData = writer.toString();
			
			Editor editor = getPreferences().edit();
			
			editor.putString(PREFERENCES_TIMERKEY_PERFIX+data.getId(), xmlData).commit();
			
		} catch(Throwable t) {
			throw new RuntimeException(t);
		}
	}
	
	private void serializeAttribute(XmlSerializer serializer, String name,String value) throws IllegalArgumentException, IllegalStateException, IOException {
		serializer.startTag(null, XML_TIMER_DATA_ATTRIBUTE);
			serializer.attribute(null, "name",name);
			serializer.text(value);
		serializer.endTag(null, XML_TIMER_DATA_ATTRIBUTE);
	}
	
	@Override
	public PersistentTimerData loadTimerData(long id) {
		
		try {
			String stringData = getPreferences().getString(PREFERENCES_TIMERKEY_PERFIX+id,null);
			
			if(stringData != null) {
				DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
				DocumentBuilder db = dbf.newDocumentBuilder();
				
		        InputStream bais = new ByteArrayInputStream(stringData.getBytes());
		        BufferedInputStream bis = new BufferedInputStream(bais);
		        Document doc = db.parse(bis);
		        
		        Node timerData = doc.getElementsByTagName(XML_TIMER_DATA).item(0);
		        
		        String endless = null;	
				String ticks = null; 	
				String currentTicks = null;	
				String timerStartedDate = null; 
				String delay = null;	
				String forceTicks = null;
				String uniqueName = null;
		        		        
		        NodeList list = timerData.getChildNodes();
		        for(int i = 0; i < list.getLength(); i++) {
		        	Node attribute = list.item(i);
		        	NamedNodeMap map = attribute.getAttributes();
		        	
		        	String name = null;
		        	String value = attribute.getTextContent();
		        	
		        	for(int i2 = 0; i2 < map.getLength(); i2++) {
		        		Node attribute2 = map.item(i2);
		        		
		        		name = attribute2.getNodeValue();
		        	}
		        	
		        	if(name.equals("endless")) {
		        		endless = value;
		        	} else if(name.equals("ticks")) {
		        		ticks = value;
		        	} else if(name.equals("currentTicks")) {
		        		currentTicks = value;
		        	} else if(name.equals("timerStartedDate")) {
		        		timerStartedDate = value;
		        	} else if(name.equals("forceTicks")) {
		        		forceTicks = value;
		        	}  else if(name.equals("delay")) {
		        		delay = value;
		        	}  else if(name.equals("forceTicks")) {
		        		forceTicks = value;
		        	} else if(name.equals("uniqueName")) {
		        		uniqueName = value;
		        	} 
		        }

		        PersistentTimerData newData = new PersistentTimerData(id, Float.parseFloat(delay), 
		        		Long.parseLong(ticks), Long.parseLong(currentTicks), Boolean.parseBoolean(endless), Boolean.parseBoolean(forceTicks),uniqueName);
		        
		        Long timerStartedDateTime = Long.parseLong(timerStartedDate);
		        
		        Date d = new Date(timerStartedDateTime);
		        newData.setTimerStartedDate(d);
		        newData.setStarted(true);
		        
		        // TODO: ACTIONS
		        
		        NodeList actionNodes = doc.getElementsByTagName(XML_TIMER_ACTIONS_ACTION);
		        for(int i = 0; i < actionNodes.getLength(); i++) {
		        	
		        	Node actionNode = actionNodes.item(i);
		        	
		        	String actionTypeName = null;
		        	String actionClassName = null;
		        	
		        	NamedNodeMap map = actionNode.getAttributes();
		        	for(int i2 = 0; i2 < map.getLength(); i2++) {
		        		Node attributeNode = map.item(i2);
		        		if(attributeNode.getNodeName().equals(XML_TIMER_ACTIONS_ACTION_CLASS)) {
		        			actionClassName = attributeNode.getNodeValue();
		        		} else if(attributeNode.getNodeName().equals(XML_TIMER_ACTIONS_ACTION_TYPE)) {
		        			actionTypeName = attributeNode.getNodeValue();
		        		}
		        	}
		        	@SuppressWarnings("unchecked")
					Class<? extends AbstractTimerAction> timerActionClass = (Class<? extends AbstractTimerAction>) Class.forName(actionClassName);
		        	AbstractTimerAction timerAction = timerActionClass.newInstance();
		        	if(timerAction instanceof ISerializableTimerAction) {
		        		ISerializableTimerAction serTimerAction = (ISerializableTimerAction)timerAction;
		        		serTimerAction.deserialize(actionNode);
		        	}
		        	
		        	if(actionTypeName.equals(XML_TIMER_ACTIONS_ACTION_TYPE_ONTICK)) {
		        		newData.addTickAction(timerAction);
		        	} else if(actionTypeName.equals(XML_TIMER_ACTIONS_ACTION_TYPE_ONFINISH)) {
		        		newData.addFinishAction(timerAction);
		        	}
		        }
		        
		        return newData;
			} 
		} catch(Throwable t) {
			throw new RuntimeException(t);
		}
		
		// no specific timer data found.. could not restore it from memory..
		return null;
	}
	
	@Override
	public boolean checkUniqueNameAvailable(String uniqueName) {
		
		String uniqueNames = getPreferences().getString(PREFERENCES_TIMER_UNIQUE_NAMES, null);
		
		if(uniqueNames != null) {
			String[] names = uniqueNames.split(";");
			
			for(String name : names) {
				if(name.equals(uniqueName)) {
					System.out.println("Unique Name " + uniqueName + " was already taken!");
					return false;
				}
			}
		}
			
		System.out.println("Unique Name " + uniqueName + " is free to take!");
		return true;
	}
	
	@Override
	public void registerUniqueName(String uniqueName) {
		if(checkUniqueNameAvailable(uniqueName)) {
			Editor editor = getPreferences().edit();
			String uniqueNames = getPreferences().getString(PREFERENCES_TIMER_UNIQUE_NAMES, null);
			uniqueNames+= ";" + uniqueName;
			editor.putString(PREFERENCES_TIMER_UNIQUE_NAMES, uniqueNames).commit();
		}	
	}

	@Override
	public void removeTimerData(PersistentTimerData data) {
		
		// removing a timer also needs to remove the unique name from the list..
		
		Editor edit = getPreferences().edit();
		
		if(data.getUniqueName() != null) {
			String uniqueNames = getPreferences().getString(PREFERENCES_TIMER_UNIQUE_NAMES, null);
			String newUniqueNames = "";
			if(uniqueNames != null) {
				// should not be null actually..
				for(String s : uniqueNames.split(";")) {
					if(!s.equals(data.getUniqueName())) {
						newUniqueNames+=";"+data.getUniqueName();
					}
				}
			}
			edit.putString(PREFERENCES_TIMER_UNIQUE_NAMES, newUniqueNames);
		}
		
		edit.remove(PREFERENCES_TIMERKEY_PERFIX+data.getId()).commit();
	}

	@Override
	public Long getNewTimerId() {		
		long newId = getPreferences().getLong(PREFERENCES_TIMER_LAST_ID, 0);
		newId++;
		getPreferences().edit().putLong(PREFERENCES_TIMER_LAST_ID, newId).commit();	
		return newId;
	}

	@Override
	public Set<Long> getTimerIds() {

		Set<Long> ids = new HashSet<Long>();		
		for(String key : getPreferences().getAll().keySet()) {
			if(key.startsWith(PREFERENCES_TIMERKEY_PERFIX)) {
			
				String idPart = key.substring(PREFERENCES_TIMERKEY_PERFIX.length());
				
				long id = Long.parseLong(idPart);
				ids.add(id);
			} 
		}		
		return ids;
	}
	
	
	private SharedPreferences getPreferences() {
		SharedPreferences preferences = this.context.getSharedPreferences(getClass().getName() + "TIMERS.options",Context.MODE_PRIVATE);
		return preferences;
	}
	
	// ** Dependency setters **
	
	public void setContext(Context context) {
		this.context = context;
	}
}